package com.xtm.exercise.base.A12JUC;

import org.junit.Test;

import java.util.concurrent.ConcurrentHashMap;

/***
 https://blog.csdn.net/qq15035899256/article/details/126222663?spm=1001.2014.3001.5502
    多线程同时访问它，不会影响map中每一条数据的一致性
    底层依然由“数组”+链表+红黑树的方式思想，大于8个转换为红黑树。默认初始大小16，负载因子也是0.75，定位元素的方法也是先hashCode(),再无符号右移16位异或，再(n-1)&hash
 取消segments字段，直接采用transient volatile Node<K,V>[] table;保存数据，采用table数组元素作为锁，从而实现了对每一行数据进行加锁，进一步减少并发冲突的概率。
 */
public class ConcurrentHashMapDemo {

    @Test
    public void test01(){
        // java.util.concurrent包下
        // initialCapacity容量 - 第一个参数表示这个map的容量是8，也就是说这个对象可以存储8个键值对.
        // loadFactor负载因子 - 这个map对象的负载因子是 0.6. 这意味着，每当我们的哈希表被填满60%的时候，条目就会被移动到一个新的哈希表，其容量大小是原来哈希表的两倍。
        ConcurrentHashMap<Object, Object> map = new ConcurrentHashMap<>(8, 0.6f);

        // 默认容量与负载因子我们还可以通过下面的代码初始化一个ConcurrentHashMap对象，默认情况下capacity=16，loadFactor=0.75
        ConcurrentHashMap<Object, Object> map1 = new ConcurrentHashMap<>();

        /*
         put函数流程：
1、判断put进来的key和value是否为null，如果为null抛异常。（ConcurrentHashMap的key、value不能为null）。
2、随后进入无限循环(没有判断条件的for循环)，何时插入成功，何时退出。
3、在无限循环中，若table数组为空（底层数组加链表），则调用initTable()，初始化table；
4、若table不为空，先hashCode(),再无符号右移16位异或，再(n-1)&hash，定位到table中的位置，如果该位置为空（说明还没有发生哈希冲突），则使用CAS将新的节点放入table中。
5、如果该位置不为空，且该节点的hash值为MOVED（即为forward节点，哈希值为-1，其中含有指向nextTable的指针，class ForwardingNode中有nexttable变量），说明此时正在扩容，且该节点已经扩容完毕，如果还有剩余任务（任务没分配完）该线程执行helpTransfer方法，帮助其他线程完成扩容，如果已经没有剩余任务，则该线程可以直接操作新数组nextTable进行put。
6、如果该位置不为空，且该节点不是forward节点。对桶中的第一个结点（即table表中的结点，哈希值相同的链表的第一个节点）进行加锁（锁是该结点，如果此时还有其他线程想来put，会阻塞）（如果不加锁，可能在遍历链表的过程中，又有其他线程放进来一个相同的元素，但此时我已经遍历过，发现没有相同的，这样就会产生两个相同的），对该桶进行遍历，桶中的结点的hash值与key值与给定的hash值和key值相等，则根据标识选择是否进行更新操作（用给定的value值替换该结点的value值），若遍历完桶仍没有找到hash值与key值和指定的hash值与key值相等的结点，则直接新生一个结点并赋值为之前最后一个结点的下一个结点。
7、若binCount值达到红黑树转化的阈值，则将桶中的结构转化为红黑树存储，最后，增加binCount的值。最后调用addcount方法，将concurrenthashmap的size加1，调用size()方法时会用到这个值。
扩容transfer()函数流程：
整个扩容操作分为两个部分
第一部分是构建一个nextTable,它的容量是原来的两倍，这个操作是单线程完成的。
第二个部分就是将原来table中的元素复制到nextTable中，这里允许多线程进行操作。
其他线程调用helptransfer方法来协助扩容时，首先拿到nextTable数组，再调用transfer方法。给新来的线程分配任务（默认是16个桶一个任务）。
遍历自己所分到的桶：
1、桶中元素不存在，则通过CAS操作设置桶中第一个元素为ForwardingNode，其Hash值为MOVED（-1）,同时该元素含有新的数组引用
此时若其他线程进行put操作，发现第一个元素的hash值为-1则代表正在进行扩容操作（并且表明该桶已经完成扩容操作了，可以直接在新的数组中重新进行hash和插入操作），该线程就可以去帮助扩容，或者没有任务则不用参与，此时可以去直接操作新的数组了
2、桶中元素存在且hash值为-1，则说明该桶已经被处理了（本不会出现多个线程任务重叠的情况，这里主要是该线程在执行完所有的任务后会再次进行检查，再次核对）
3、桶中为链表或者红黑树结构，则需要获取桶锁，防止其他线程对该桶进行put操作，然后处理方式同HashMap的处理方式一样，对桶中元素分为2类，分别代表当前桶中和要迁移到新桶中的元素。设置完毕后代表桶迁移工作已经完成，旧数组中该桶可以设置成ForwardingNode了，已经完成从table复制到nextTable的节点，要设置为forward
get函数流程：
1、根据k计算出hash值，找到对应的数组index
2、如果该index位置无元素则直接返回null
3、如果该index位置有元素
如果第一个元素的hash值小于0，则该节点可能为ForwardingNode或者红黑树节点TreeBin
如果是ForwardingNode（表示当前正在进行扩容，且已经扩容完成），使用新的数组来进行查找
如果是红黑树节点TreeBin，使用红黑树的查找方式来进行查找
如果第一个元素的hash大于等于0，则为链表结构，依次遍历即可找到对应的元素，也就是读的时候不会加锁，同时有put，不会阻塞。
读不加锁是因为使用了volatile（用在transient volatile Node<K,V>[] table），happens-before

ConcurrentHashmap和Hashtable不允许key和value为null：
ConcurrentHashmap和Hashtable都是支持并发的，这样会有一个问题，当你通过get(k)获取对应的value时，如果获取到的是null时，你无法判断，它是put（k,v）的时候value为null，还是这个key从来没有做过映射。HashMap是非并发的，可以通过contains(key)来做这个判断。而支持并发的Map在调用m.contains（key）和m.get(key),m可能已经不同了。
ConcurrentHashmap和Hashtable都不允许key和value为null，Collections.synchronizedMap和HashMap的key和value都可以为null（因为就是包装了hashmap），TreeMap的key不可为空（非线程安全，需要排序），value可以。
         **/
        map1.put(null,null);

        /*
         get函数流程：
1、根据k计算出hash值，找到对应的数组index
2、如果该index位置无元素则直接返回null
3、如果该index位置有元素
如果第一个元素的hash值小于0，则该节点可能为ForwardingNode或者红黑树节点TreeBin
如果是ForwardingNode（表示当前正在进行扩容，且已经扩容完成），使用新的数组来进行查找
如果是红黑树节点TreeBin，使用红黑树的查找方式来进行查找
如果第一个元素的hash大于等于0，则为链表结构，依次遍历即可找到对应的元素，也就是读的时候不会加锁，同时有put，不会阻塞。
读不加锁是因为使用了volatile（用在transient volatile Node<K,V>[] table），happens-before

ConcurrentHashmap和Hashtable不允许key和value为null：
ConcurrentHashmap和Hashtable都是支持并发的，这样会有一个问题，当你通过get(k)获取对应的value时，如果获取到的是null时，你无法判断，它是put（k,v）的时候value为null，还是这个key从来没有做过映射。HashMap是非并发的，可以通过contains(key)来做这个判断。而支持并发的Map在调用m.contains（key）和m.get(key),m可能已经不同了。
ConcurrentHashmap和Hashtable都不允许key和value为null，Collections.synchronizedMap和HashMap的key和value都可以为null（因为就是包装了hashmap），TreeMap的key不可为空（非线程安全，需要排序），value可以。
         */

        map1.get("a");
    }
}
